package com.superkripts.sktools;

import ch.njol.skript.Skript;
import ch.njol.skript.classes.ClassInfo;
import ch.njol.skript.lang.Expression;
import ch.njol.skript.lang.function.FunctionEvent;
import ch.njol.skript.lang.function.JavaFunction;
import ch.njol.skript.lang.function.Parameter;
import ch.njol.skript.registrations.Classes;
import com.superkripts.sktools.annotation.Param;
import com.superkripts.sktools.value.BaseDefaultValue;
import com.superkripts.sktools.value.CustomDefaultValue;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.signature.SignatureWriter;

import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;

class MethodFunctionBuilder implements Opcodes {

    private String name;
    private String since;
    private String[] description;
    private String[] examples;
    private String[] requiredPlugin;
    private Object instance;

    private MethodFunctionBuilder() {

    }

    public MethodFunctionBuilder name(@NotNull String name) {
        this.name = name;
        return this;
    }

    public MethodFunctionBuilder since(String since) {
        this.since = since;
        return this;
    }

    public MethodFunctionBuilder description(String[] description) {
        this.description = description;
        return this;
    }

    public MethodFunctionBuilder examples(String[] examples) {
        this.examples = examples;
        return this;
    }

    public MethodFunctionBuilder requiredPlugin(String[] requiredPlugin) {
        this.requiredPlugin = requiredPlugin;
        return this;
    }

    public MethodFunctionBuilder instance(Object instance) {
        this.instance = instance;
        return this;
    }

    public JavaFunction<?> build(Method method) {
        boolean single = true;
        Class<?> returnTypeClass = method.getReturnType();
        // ! 多维数组可能会有问题
        if (returnTypeClass.isArray()) {
            returnTypeClass = returnTypeClass.getComponentType();
            single = false;
        }
        if (returnTypeClass.isPrimitive()) {
            if (returnTypeClass == void.class) {
                returnTypeClass = Object.class;
            } else if (returnTypeClass == byte.class) {
                returnTypeClass = Byte.class;
            } else if (returnTypeClass == short.class) {
                returnTypeClass = Short.class;
            } else if (returnTypeClass == int.class) {
                returnTypeClass = Integer.class;
            } else if (returnTypeClass == long.class) {
                returnTypeClass = Long.class;
            } else if (returnTypeClass == float.class) {
                returnTypeClass = Float.class;
            } else if (returnTypeClass == double.class) {
                returnTypeClass = Double.class;
            } else if (returnTypeClass == boolean.class) {
                returnTypeClass = Boolean.class;
            } else if (returnTypeClass == char.class) {
                returnTypeClass = Character.class;
            }
        }
        Class<?> returnArrayTypeClass = Array.newInstance(returnTypeClass, 0).getClass();
        String functionName = name == null ? method.getName() : name;
        String className = method.getDeclaringClass().getName() + "$" + method.getName();
        String internalClassName = Type.getInternalName(method.getDeclaringClass()) + "$" + method.getName();

        // 变量声明
        MethodVisitor mv;
        SignatureWriter sw;
        String descriptor, signature;
        // 手搓 class
        ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        {
            // 泛型 JavaFunction<{returnTypeClass}>
            sw = new SignatureWriter();
            sw.visitClassType(Type.getInternalName(JavaFunction.class));
            sw.visitTypeArgument('=');
            sw.visitClassType(Type.getInternalName(returnTypeClass));
            sw.visitEnd();
            sw.visitEnd();
            signature = sw.toString();
        }
        // public final class {internalClassName} extends JavaFunction<{returnTypeClass}>
        classWriter.visit(V1_8, ACC_PUBLIC | ACC_FINAL | ACC_SUPER, internalClassName, signature, Type.getInternalName(JavaFunction.class), null);
        // source <{simpleClassName}${methodName}.java>
        classWriter.visitSource(method.getDeclaringClass().getSimpleName() + "$" + method.getName() + ".java", null);
        // field
        {
            if (!Modifier.isStatic(method.getModifiers())) {
                classWriter.visitField(ACC_PRIVATE | ACC_FINAL, "functionClassInstance", Type.getDescriptor(method.getDeclaringClass()), null, null).visitEnd();
            }
        }
        // constructor
        {
            int varMethodIndex;
            if (Modifier.isStatic(method.getModifiers())) {
                descriptor = Type.getMethodDescriptor(Type.VOID_TYPE, Type.getType(Method.class));
                varMethodIndex = 1;
            } else {
                descriptor = Type.getMethodDescriptor(Type.VOID_TYPE, Type.getType(method.getDeclaringClass()), Type.getType(Method.class));
                varMethodIndex = 2;
            }
            // <init>(Method)
            mv = classWriter.visitMethod(ACC_PUBLIC, "<init>", descriptor, null, null);
            mv.visitCode();
            // parameter[] param2 = method.getParameters()
            mv.visitVarInsn(ALOAD, varMethodIndex);
            descriptor = Type.getMethodDescriptor(Type.getType(java.lang.reflect.Parameter[].class));
            mv.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(Method.class), "getParameters", descriptor, false);
            mv.visitVarInsn(ASTORE, varMethodIndex + 1);
            // push this
            mv.visitVarInsn(ALOAD, 0);
            // push name
            mv.visitLdcInsn(functionName);
            // push parameters
            java.lang.reflect.Parameter[] methodParameters = method.getParameters();
            mv.visitIntInsn(BIPUSH, methodParameters.length);
            mv.visitTypeInsn(ANEWARRAY, Type.getInternalName(Parameter.class)); // new parameter[{parameterCount}]
            for (int i = 0; i < methodParameters.length; i++) {
                java.lang.reflect.Parameter methodParameter = methodParameters[i];
                Param paramAnnotation = methodParameter.getAnnotation(Param.class);
                boolean paramSingle = true;
                Class<?> functionParameterType = methodParameter.getType();
                if (functionParameterType.isArray()) {
                    paramSingle = false;
                    functionParameterType = functionParameterType.getComponentType();
                }
                String functionParameterName = paramAnnotation == null || paramAnnotation.value().isEmpty() ? methodParameter.getName() : paramAnnotation.value();
                // parameter[{i}]
                mv.visitInsn(DUP);
                mv.visitIntInsn(BIPUSH, i);
                // new parameter()
                mv.visitTypeInsn(NEW, Type.getInternalName(Parameter.class));
                // push parameter
                mv.visitInsn(DUP);
                // push name
                mv.visitLdcInsn(functionParameterName);
                // push classInfo
                mv.visitLdcInsn(Type.getType(functionParameterType));
                descriptor = Type.getMethodDescriptor(Type.getType(ClassInfo.class), Type.getType(Class.class));
                mv.visitMethodInsn(INVOKESTATIC, Type.getInternalName(Classes.class), "getExactClassInfo", descriptor, false);
                // push single
                mv.visitInsn(paramSingle ? ICONST_1 : ICONST_0);
                // push def
                if (paramAnnotation != null && (paramAnnotation.defaultType() != CustomDefaultValue.class || !paramAnnotation.defaultValue().isEmpty())) {
                    Class<? extends CustomDefaultValue> defaultType = paramAnnotation.defaultType();
                    if (defaultType == CustomDefaultValue.class) {
                        defaultType = BaseDefaultValue.class;
                    }
                    mv.visitLdcInsn(Type.getType(defaultType));
                    descriptor = Type.getMethodDescriptor(Type.getType(CustomDefaultValue.class), Type.getType(Class.class));
                    mv.visitMethodInsn(INVOKESTATIC, Type.getInternalName(CustomDefaultValue.class), "getCustomDefaultValueInstance", descriptor, false);
                    mv.visitVarInsn(ALOAD, varMethodIndex + 1);
                    mv.visitIntInsn(BIPUSH, i);
                    mv.visitInsn(AALOAD);
                    mv.visitLdcInsn(paramAnnotation.defaultValue());
                    descriptor = Type.getMethodDescriptor(Type.getType(Expression.class), Type.getType(java.lang.reflect.Parameter.class), Type.getType(String.class));
                    mv.visitMethodInsn(INVOKEVIRTUAL, Type.getInternalName(CustomDefaultValue.class), "getDefaultValue", descriptor, false);
                } else {
                    mv.visitInsn(ACONST_NULL); // push null
                }
                // invoke super
                String parameterConstructorDescriptor = getConstructorDescriptor(Parameter.class, String.class, ClassInfo.class, boolean.class, Expression.class);
                mv.visitMethodInsn(INVOKESPECIAL, Type.getInternalName(Parameter.class), "<init>", parameterConstructorDescriptor, false);
                // this.functionClassInstance = functionClassInstance;
                if (!Modifier.isStatic(method.getModifiers())) {
                    mv.visitVarInsn(ALOAD, 0);
                    mv.visitVarInsn(ALOAD, 1);
                    mv.visitFieldInsn(PUTFIELD, internalClassName, "functionClassInstance", Type.getDescriptor(method.getDeclaringClass()));
                }
                mv.visitInsn(AASTORE);
            }
            // push returnType
            mv.visitLdcInsn(Type.getType(returnTypeClass));
            descriptor = Type.getMethodDescriptor(Type.getType(ClassInfo.class), Type.getType(Class.class));
            mv.visitMethodInsn(INVOKESTATIC, Type.getInternalName(Classes.class), "getExactClassInfo", descriptor, false);
            mv.visitInsn(single ? ICONST_1 : ICONST_0); // single
            // invoke super
            descriptor = getConstructorDescriptor(JavaFunction.class, String.class, Parameter[].class, ClassInfo.class, boolean.class);
            mv.visitMethodInsn(INVOKESPECIAL, Type.getInternalName(JavaFunction.class), "<init>", descriptor, false);
            // return
            mv.visitInsn(RETURN);
            mv.visitMaxs(0, 0);
            mv.visitEnd();
        }
        // execute
        execute:
        {
            descriptor = Type.getMethodDescriptor(Type.getType(returnArrayTypeClass), Type.getType(FunctionEvent.class), Type.getType(Object[][].class));
            {
                // "(FunctionEvent<*>, Object[][]) {returnTypeClass}[]"
                sw = new SignatureWriter();
                sw.visitParameterType();
                sw.visitClassType(Type.getInternalName(FunctionEvent.class));
                sw.visitTypeArgument();
                sw.visitEnd();
                sw.visitClassType(Type.getInternalName(Object[][].class));
                sw.visitEnd();
                sw.visitReturnType();
                sw.visitClassType(Type.getInternalName(returnArrayTypeClass));
                sw.visitEnd();
                signature = sw.toString();
            }
            mv = classWriter.visitMethod(ACC_PUBLIC, "execute", descriptor, signature, null);
            // check requestPlugins
            String nonPlugins = Arrays.stream(requiredPlugin).filter(pl -> !Bukkit.getPluginManager().isPluginEnabled(pl)).collect(Collectors.joining(", ", "[", "]"));
            if (!nonPlugins.equals("[]")) {
                mv.visitCode();
                mv.visitLdcInsn("Error executing function '" + functionName + "()', Missing necessary plugins: " + nonPlugins);
                descriptor = Type.getMethodDescriptor(Type.VOID_TYPE, Type.getType(String.class));
                mv.visitMethodInsn(INVOKESTATIC, Type.getInternalName(Skript.class), "error", descriptor, false);
                mv.visitInsn(ACONST_NULL);
                mv.visitInsn(ARETURN);
                mv.visitMaxs(0, 0);
                mv.visitEnd();
                break execute;
            }
            // code
            mv.visitCode();
            Label returnLabel = new Label();
            if (method.getReturnType() != void.class && single) {
                mv.visitInsn(ICONST_1);
                mv.visitTypeInsn(ANEWARRAY, Type.getInternalName(returnTypeClass));
                mv.visitInsn(DUP);
                mv.visitInsn(ICONST_0);
            }
            // push this.functionClassInstance
            if (!Modifier.isStatic(method.getModifiers())) {
                mv.visitVarInsn(ALOAD, 0);
                mv.visitFieldInsn(GETFIELD, internalClassName, "functionClassInstance", Type.getDescriptor(method.getDeclaringClass()));
            }
            java.lang.reflect.Parameter[] methodParameters = method.getParameters();
            for (int i = 0; i < methodParameters.length; i++) {
                boolean nullable = false;
                java.lang.reflect.Parameter methodParameter = methodParameters[i];
                Param paramAnnotation = methodParameter.getAnnotation(Param.class);
                if (paramAnnotation != null) {
                    nullable = paramAnnotation.defaultType() != CustomDefaultValue.class || !paramAnnotation.defaultValue().isEmpty() || paramAnnotation.nullable();
                }
                Label pushNullLabel = new Label();
                Label continueLabel = new Label();
                mv.visitVarInsn(ALOAD, 2);
                mv.visitIntInsn(BIPUSH, i);
                mv.visitInsn(AALOAD);
                mv.visitVarInsn(ASTORE, 3);
                mv.visitVarInsn(ALOAD, 3);
                mv.visitJumpInsn(IFNULL, nullable ? pushNullLabel : returnLabel);
                mv.visitVarInsn(ALOAD, 3);
                mv.visitInsn(ARRAYLENGTH);
                mv.visitJumpInsn(IFEQ, nullable ? pushNullLabel : returnLabel);
                mv.visitVarInsn(ALOAD, 3);
                mv.visitInsn(ICONST_0);
                mv.visitInsn(AALOAD);
                mv.visitJumpInsn(IFNULL, nullable ? pushNullLabel : returnLabel);
                // push param
                Class<?> parameterType = methodParameter.getType();
                mv.visitVarInsn(ALOAD, 2);
                mv.visitIntInsn(BIPUSH, i);
                mv.visitInsn(AALOAD);
                if (parameterType.isArray()) {
                    mv.visitTypeInsn(CHECKCAST, Type.getInternalName(parameterType));
                } else {
                    mv.visitInsn(ICONST_0);
                    mv.visitInsn(AALOAD);
                    mv.visitTypeInsn(CHECKCAST, Type.getInternalName(parameterType));
                }
                mv.visitJumpInsn(GOTO, continueLabel);
                mv.visitLabel(pushNullLabel);
                mv.visitInsn(ACONST_NULL);
                mv.visitLabel(continueLabel);
            }
            // invoke method
            descriptor = Type.getMethodDescriptor(method);
            int invokeOpcode = Modifier.isStatic(method.getModifiers()) ? INVOKESTATIC : INVOKEVIRTUAL;
            mv.visitMethodInsn(invokeOpcode, Type.getInternalName(method.getDeclaringClass()), method.getName(), descriptor, false);

            if (method.getReturnType() != void.class) {
                if (single) {
                    mv.visitInsn(AASTORE);
                } else {
                    mv.visitInsn(POP);
                    mv.visitInsn(ACONST_NULL);
                }
                mv.visitInsn(ARETURN);
            }
            // return object[0]
            mv.visitInsn(ICONST_0);
            mv.visitTypeInsn(ANEWARRAY, Type.getInternalName(returnTypeClass));
            mv.visitInsn(ARETURN);
            // return null
            mv.visitLabel(returnLabel);
            mv.visitInsn(ACONST_NULL);
            mv.visitInsn(ARETURN);
            mv.visitMaxs(0, 0);
            mv.visitEnd();
        }
        // bridge method
        if (returnTypeClass != Object.class) {
            descriptor = Type.getMethodDescriptor(Type.getType(Object[].class), Type.getType(FunctionEvent.class), Type.getType(Object[][].class));
            mv = classWriter.visitMethod(ACC_PUBLIC | ACC_BRIDGE | ACC_SYNTHETIC, "execute", descriptor, null, null);
            mv.visitCode();
            mv.visitVarInsn(ALOAD, 0);
            mv.visitVarInsn(ALOAD, 1);
            mv.visitVarInsn(ALOAD, 2);
            descriptor = Type.getMethodDescriptor(Type.getType(returnArrayTypeClass), Type.getType(FunctionEvent.class), Type.getType(Object[][].class));
            mv.visitMethodInsn(INVOKEVIRTUAL, internalClassName, "execute", descriptor, false);
            mv.visitInsn(ARETURN);
            mv.visitMaxs(0, 0);
            mv.visitEnd();
        }
        classWriter.visitEnd();

        Class<JavaFunction<?>> methodFunctionClass = MethodFunctionClassLoader.INSTANCE.defineClass(className, classWriter.toByteArray());
        try {
            JavaFunction<?> javaFunction;
            if (Modifier.isStatic(method.getModifiers())) {
                javaFunction = methodFunctionClass.getConstructor(Method.class).newInstance(method);
            } else {
                javaFunction = methodFunctionClass.getConstructor(method.getDeclaringClass(), Method.class).newInstance(instance, method);
            }
            if (since != null) {
                javaFunction.since(since);
            }
            if (description != null) {
                javaFunction.description(description);
            }
            if (examples != null) {
                javaFunction.examples(examples);
            }
            return javaFunction;
        } catch (ReflectiveOperationException e) {
            throw new IllegalStateException(e);
        }
    }

    public static MethodFunctionBuilder builder() {
        return new MethodFunctionBuilder();
    }

    private static String getConstructorDescriptor(Class<?> clazz, Class<?>... parameterTypes) {
        try {
            return Type.getConstructorDescriptor(clazz.getConstructor(parameterTypes));
        } catch (NoSuchMethodException e) {
            throw new IllegalStateException(e);
        }
    }

    static class MethodFunctionClassLoader extends ClassLoader {
        public static final MethodFunctionClassLoader INSTANCE = new MethodFunctionClassLoader(SkriptTools.getInstance().getClass().getClassLoader());

        private final Map<String, byte[]> classes = new HashMap<>();

        private MethodFunctionClassLoader(ClassLoader parent) {
            super(parent);
        }

        @SuppressWarnings("unchecked")
        public Class<JavaFunction<?>> defineClass(String name, byte[] data) {
            if (classes.containsKey(name)) {
                throw new IllegalArgumentException();
            }
            Class<JavaFunction<?>> clazz = (Class<JavaFunction<?>>) defineClass(name, data, 0, data.length);
            classes.put(name, data);
            return clazz;
        }

        public Map<String, byte[]> getClasses() {
            return Collections.unmodifiableMap(classes);
        }
    }
}
